package com.yswee.framework.net.http;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.GZIPInputStream;

import org.apache.http.protocol.HTTP;

import com.google.gson.Gson;
import com.yswee.framework.Application;
import com.yswee.framework.net.ITask;
import com.yswee.framework.net.engine.IEngineTask;
import com.yswee.framework.net.engine.ThreadPoolExecutor;
import com.yswee.framework.utils.HttpUtils;
import com.yswee.framework.utils.LogcatUtils;
import com.yswee.framework.utils.PackageUtils;
import com.yswee.framework.utils.StringUtils;

import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.core.content.FileProvider;
import android.text.TextUtils;

class Task<TPARSED> implements IEngineTask, ITask {
	
	private static final String TAG = Task.class.getSimpleName();

	protected static final int BUFFER_COUNT = 2;
	protected static final int BUFFER_SIZE = 1024 * 8;
	protected static int MAXRETRYTIME = 2;
	
	protected int currRetryTime = 0;
	protected boolean isCanceled = false;
    
	protected Entity<TPARSED> mEntity;
	protected Callback<TPARSED> mListener;

	Handler mHandler = new Handler(Looper.getMainLooper()) {
		@Override
		public void handleMessage(Message msg) {
			int what = msg.what;
			switch (what) {
			case Entity.DOWNLOADPROGRESS:
				mEntity.setEntityStatus(what);
				handleDownloadProgress(msg.arg1, msg.arg2);
				//LogcatUtils.i(TAG, "response pregress = " + msg.arg1 + "/" + msg.arg2);
				break;
			case Entity.UPLOADPROGRESS:
				mEntity.setEntityStatus(what);
				handleUploadProgress(msg.arg1, msg.arg2);
				//LogcatUtils.i(TAG, "request pregress = " + msg.arg1 + "/" + msg.arg2);
				break;
			case Entity.FINISHED:
				mEntity.setEntityStatus(what);
				if (mListener != null)
					mListener.onFinish(mEntity);
				unregistTask();
				break;
			case Entity.ERROR:
				if(HttpUtils.isNetWorkConnected(Application.get())) {
					mEntity.setEntityStatus(what);
				} else {
					mEntity.setEntityStatus(Entity.NETINVALID);
				}
				if (mListener != null) {
					mListener.onError(mEntity);
				}
				unregistTask();
				break;
			case Entity.NEEDLOGIN:
				mEntity.setEntityStatus(what);
				if (mListener != null) {
					mListener.onError(mEntity);
				}
				unregistTask();
				break;
			case Entity.CENCELED:
				mEntity.setEntityStatus(what);
				if (mListener != null)
					mListener.onCancel(mEntity);
				unregistTask();
				break;
			case Entity.INVALID:
				mEntity.setEntityStatus(what);
				if (mListener != null)
					mListener.onInvalid(mEntity);
				break;
			default:
				break;
			}
		};
	};
	
	Task (Entity<TPARSED> entity, Callback<TPARSED> listener) {
		mEntity = entity;
		mListener = listener;
		if (Application.get().getAppConfigure().getIsDebug()) {
			MAXRETRYTIME = 1;
		}
	}

	@Override
	public int getPriority() {
		if(mEntity != null) {
			return mEntity.getPriority();
		}
		return IEngineTask.DEFAULTPRIORITY;
	}

	@Override
	public boolean isValid() {
		if(isCanceled)
			return false;
		if(mEntity != null) {
			boolean valid = mEntity.isValid();
			if(!valid) {
				mHandler.sendEmptyMessage(Entity.INVALID);
			}
			return valid;
		}
		return true;
	}

	@Override
	public void notifyWake() {
		
	}

	@Override
	public void excute() {
		if(mEntity != null) {
			ThreadPoolExecutor.get().execute(this);
			registTask();
		}
	}

	protected void registTask() {
		if(mEntity.getContext() != null) {
			mEntity.getContext().registTask(this);
		}
	}
	
	protected void unregistTask() {
		if(mEntity.getContext() != null) {
			mEntity.getContext().unregistTask(this);
		}
	}

	@Override
	public void cancel() {
		isCanceled = true;
		ThreadPoolExecutor.get().remove(this);
		LogcatUtils.i(TAG, "cancel task url : " + mEntity.getBaseUrl());
	}

	@Override
	public void run() {
		try {
			if(TextUtils.isEmpty(mEntity.getBaseUrl())) {
				mEntity.setMessage("base url is empty");
	        	mHandler.sendEmptyMessage(Entity.ERROR);
	        	return;
			}
			if (isCanceled) {
                mHandler.sendEmptyMessage(Entity.CENCELED);
                return;
            }
            long startTime = System.currentTimeMillis();
            currRetryTime = 0;
            excuteHttpRequest();
            LogcatUtils.i(TAG, "execute task time = " + (System.currentTimeMillis() - startTime) + "ms");
        } catch (OutOfMemoryError e) {
        	LogcatUtils.e(TAG, e);
        }
	}
	
	protected void excuteHttpRequest() {
        String errormsg = "";
		if(currRetryTime++ >= MAXRETRYTIME) {
			errormsg = "connection has runout max retry time";
		} else {
        	if(mListener != null)
        		mListener.onBeginRequest(mEntity);
        	HttpURLConnection conn = null;
	        try {
	            String baseUrl = mEntity.getBaseUrl();
	            if (baseUrl == null || "".equals(baseUrl)) {
	            	errormsg = "connection url is empty";
	            } else {
		            LogcatUtils.i(TAG, "request [" + mEntity.getHttpType() + "] url = " + baseUrl);
		            if (HttpUtils.isNetWorkConnected(Application.get())) {
		            	conn = initHttpConnection();
			            int code = 0;
			            try {
			                if (isCanceled) {
			                    mHandler.sendEmptyMessage(Entity.CENCELED);
			                    return;
			                }
			                conn.connect();
			                excuteHttpPost(conn);
			                if (isCanceled) {
			                    mHandler.sendEmptyMessage(Entity.CENCELED);
			                    return;
			                }
			                code = conn.getResponseCode();
			                mEntity.setResponseCode(code);
			            } catch (SocketTimeoutException e) {
			            	errormsg = "SocketTimeoutException : " + e.getMessage();
			            } catch (UnknownHostException e) {
			            	errormsg = "UnknownHostException : " + e.getMessage();
			            } catch (Exception e) {
			            	errormsg = "Exception : " + e.getMessage();
			            }
			            LogcatUtils.i(TAG, "response code = " + code);
			            // 201--206 代表成功请求
			            if (code >= HttpURLConnection.HTTP_OK && 
			            	code <= HttpURLConnection.HTTP_PARTIAL) {
			            	excuteHttpResponse(conn);
			            // 301 302重定向
			            } else if (code == HttpURLConnection.HTTP_MOVED_TEMP ||
			            		code == HttpURLConnection.HTTP_MOVED_PERM) {
			            	String newurl = conn.getHeaderField("location");
			            	if (!StringUtils.isEmpty(newurl)) {
//			            		if (newurl.toLowerCase().contains(ApiConstant.API_SYSTEM_LOGIN.toLowerCase())) {
//				                	mEntity.setMessage(errormsg);
//				                	mHandler.sendEmptyMessage(Entity.NEEDLOGIN);
//			            		} else {
				            		mEntity.setBaseUrl(newurl);
				            		excuteHttpRequest();
//			            		}
			            		return;
			            	} else {
			            		errormsg = "new url is empty, response code:" + code;
			            	}
			            // 500 -- 505 服务器错误，重试http请求
			            } else if (mEntity.isRetryOnError() && (code <= 0 || 
			               (code >= HttpURLConnection.HTTP_INTERNAL_ERROR && 
			            	code <= HttpURLConnection.HTTP_VERSION))) {
			                LogcatUtils.e(TAG, "retry connect, prev response code:" + code);
			                destroyHttpConnection(conn);
			                conn = null;
	                        Thread.sleep(500);
			                excuteHttpRequest();
			                return;
			            } else {
			            	errormsg = "No Exception, response code:" + code;
			            }
		            } else {
		            	errormsg = "network not available";
		            }
	            }
	        } catch (Exception e) {
	        	errormsg = e.getMessage();
	        } finally {
	            destroyHttpConnection(conn);
	        }
		}
        if(!TextUtils.isEmpty(errormsg)) {
        	mEntity.setMessage(errormsg);
        	mHandler.sendEmptyMessage(Entity.ERROR);
        	LogcatUtils.e(TAG, errormsg);
        }
    }

	protected HttpURLConnection initHttpConnection() {
		HttpURLConnection conn = null;
        try {
        	HttpUtils.trustAllHosts();
        	conn = HttpUtils.createHttpConnection(
        			mEntity.getBaseUrl(), getResponseIsStream());
			initConnectionParams(conn);
        	if(mListener != null)
        		mListener.onBeginConnection(conn);
		} catch (Exception e) {
			LogcatUtils.e(TAG, e);
		}
        return conn;
	}
	
	@SuppressWarnings("deprecation")
	protected void initConnectionParams(HttpURLConnection conn) {
		try {
			HttpType httpType = mEntity.getHttpType();
			if(httpType == null) {
				httpType = HttpType.Get;
			}
			switch (httpType) {
			case Post:
				conn.setRequestMethod("POST");
				break;
			case Put:
				conn.setRequestMethod("PUT");
				break;
			case Delete:
				conn.setRequestMethod("DELETE");
				break;
			case Get:
			default:
				conn.setRequestMethod("GET");
				break;
			}
			switch (httpType) {
			case Post:
			case Put:
				if(HttpUtils.isNetWork2G(Application.get())) {
					conn.setConnectTimeout(Params.HTTPPOST_CONN_TIME_OUT_LONG);
				    conn.setReadTimeout(Params.HTTPPOST_READ_TIME_OUT_LONG);
		    	} else {
					conn.setConnectTimeout(Params.HTTPPOST_CONN_TIME_OUT);
				    conn.setReadTimeout(Params.HTTPPOST_READ_TIME_OUT);
		    	}
				// disable cache
				conn.setUseCaches(false);
		        conn.setFixedLengthStreamingMode(mEntity.getContentLength());
		        
			    conn.setDoOutput(true);
			    conn.setRequestProperty(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE);
		    	conn.setRequestProperty(HTTP.CONTENT_LEN, String.valueOf(mEntity.getContentLength()));
			    if(mEntity.getPostType() == PostType.MULTIPART) {
			    	conn.setRequestProperty(HTTP.CONTENT_TYPE, "multipart/form-data; boundary=" + 
			    			mEntity.getBoundary());
			    } else if(mEntity.getPostType() == PostType.JSON) {
			    	conn.setRequestProperty(HTTP.CONTENT_TYPE, "application/json");
			    } else if(mEntity.getPostType() == PostType.BYTES) {
			    	conn.setRequestProperty(HTTP.CONTENT_TYPE, "multipart/form-data");
			    } else {
			    	conn.setRequestProperty(HTTP.CONTENT_TYPE, "application/x-www-form-urlencoded");
			    }
				break;
			case Delete:
			case Get:
			default:
				if (mEntity.getGetType() == GetType.BYTES) {
				    conn.setConnectTimeout(Params.HTTPGET_CONN_TIME_OUT_LONG);
				    conn.setReadTimeout(Params.HTTPGET_READ_TIME_OUT_LONG);
				    conn.setRequestProperty(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE);
					conn.setRequestProperty(HTTP.CONTENT_TYPE, HTTP.OCTET_STREAM_TYPE);
				} else {
				    conn.setConnectTimeout(Params.HTTPGET_CONN_TIME_OUT);
				    conn.setReadTimeout(Params.HTTPGET_READ_TIME_OUT);
					conn.setRequestProperty(HTTP.CONTENT_TYPE, HTTP.PLAIN_TEXT_TYPE);
				}
				break;
			}
		} catch (Exception e) {
			LogcatUtils.e(TAG, e.getMessage());
		}
	}
	
	protected void excuteHttpPost(HttpURLConnection conn) {
		try {
			if(mEntity.getHttpType() == HttpType.Post ||
			   mEntity.getHttpType() == HttpType.Put) {
	        	setUploadProgress(0, mEntity.getContentLength());
	            LogcatUtils.i(TAG, "request contentLength = " + mEntity.getContentLength());
				DataOutputStream output = new DataOutputStream(conn.getOutputStream());
				if(Application.get().getAppConfigure().getIsDebug()) {
					HashMap<String, Object> postDatas = mEntity.getPostData();
					StringBuilder sbPostData = new StringBuilder();
					if(postDatas != null) {
						if (mEntity.getPostType() == PostType.BYTES) {
							// nothing
							sbPostData.append("[bytes]");
						} else if (mEntity.getPostType() == PostType.JSON) {
							Gson gson = new Gson();
							sbPostData.append(gson.toJson(postDatas));
						} else {
							Iterator<Entry<String, Object>> iter = postDatas.entrySet().iterator();  
							while (iter.hasNext()) {  
								Entry<String, Object> entry = (Entry<String, Object>) iter.next();  
								String key = entry.getKey();  
								Object val = entry.getValue();
								if (!StringUtils.isEmpty(key)) {
									sbPostData.append(key).append("=");
								}
								if(!(val instanceof byte[])) {
									if (val != null) {
										sbPostData.append(val.toString());
									}
								} else {
									if (val != null) {
										sbPostData.append(new String((byte[]) val));
									}
								}
								sbPostData.append("&");
							} 
							if (sbPostData.length() > 0) {
								sbPostData.setLength(sbPostData.length() - 1);
							}
						}
					}
					LogcatUtils.i(TAG, "request postData = " + sbPostData.toString());
				}
				ArrayList<Object> postBytes = mEntity.getPostBytes();
				if(postBytes != null) {
			        int writeLen = 0;
			        int blockNum = 0;
					for(int i = 0; i < postBytes.size(); i++) {
						Object value = postBytes.get(i);
						if(value != null) {
							if (isCanceled) {
				                break;
				            }
							InputStream is = null;
							if(value instanceof byte[]) {
								is = new ByteInputStream((byte[]) value);
							} else if(value instanceof Uri) {
								Uri uri = (Uri) value;
								try {
									is = Application.get().getContentResolver().openInputStream(uri);
								} catch (SecurityException e) {
									//if file open with third party application
									if (uri.toString().contains("/storage/emulated/0")) {
										Context context = Application.get();
										String filepath_tmp = "/storage/emulated/0" + uri.toString().split("/storage/emulated/0")[1];
										uri = FileProvider.getUriForFile(context, PackageUtils.getPackageName(context) + ".fileProvider",
												new File(filepath_tmp));
										is = Application.get().getContentResolver().openInputStream(uri);
									}
								}
							}
							if(is != null) {
								byte[] buffer = new byte[BUFFER_SIZE];  
					            int len = 0;  
					            while ((len = is.read(buffer)) != -1) {  
					            	output.write(buffer, 0, len);
					            	writeLen += len;
									if ((blockNum + 1) % BUFFER_COUNT == 0) {
						                Message msg = Message.obtain();
						                msg.what = Entity.UPLOADPROGRESS;
						                msg.arg1 = writeLen;
						                msg.arg2 = mEntity.getContentLength();
						                mHandler.sendMessage(msg);
						            }
						            blockNum++;
					            }
							}
						}
					}
				}
				output.flush();
				output.close();
		    	setUploadProgress(mEntity.getContentLength(), mEntity.getContentLength());
			}
		} catch (Exception e) {
			LogcatUtils.e(TAG, e.getMessage());
		}
	}
	
	private class ByteInputStream extends InputStream {
		
		private byte[] bytes;
		private int start;
		
		public ByteInputStream(byte[] value) {
			bytes = value;
		}

		@Override
		public int read() throws IOException {
			return -1;
		}
		
		public int read(byte[] buffer) {
			int len = bytes.length - start;
			len = Math.min(len, buffer.length);
			if(len <= 0)
				return -1;
			System.arraycopy(bytes, start, buffer, 0, len);
			start += len;
			return len;
		}
	}
    
	protected void excuteHttpResponse(HttpURLConnection conn) {
		String errormsg = "";
        try {
        	if(mListener != null)
        		mListener.onBeginResponse(conn);
            LogcatUtils.i(TAG, "response contentLength = " + conn.getContentLength());
        	setDownloadProgress(0, conn.getContentLength());
            if(getResponseIsStream()) {
            	byte[] data = getStreamData(conn);
                if (isCanceled) {
                	mHandler.sendEmptyMessage(Entity.CENCELED);
                	return;
                } else if (data == null) {
                	errormsg = "data is null";
                } else {
                	setDownloadProgress(data.length, data.length);
                	mEntity.setResponseData(data);
                    data = null;
                }
            } else {
            	byte[] data = null;
            	String encoding = conn.getContentEncoding();
                LogcatUtils.i(TAG, "response encoding = " + encoding);
                if (encoding != null && "gzip".equalsIgnoreCase(encoding)) {
                	data = getGZipData(conn);
                } else {
                	data = getStreamData(conn);
                }
                if (isCanceled) {
                    mHandler.sendEmptyMessage(Entity.CENCELED);
                    return;
                } else if (data == null) {
                    errormsg = "data is null";
                } else {
                	setDownloadProgress(data.length, data.length);
                	mEntity.setResponseData(data);
                	parseResponseStream(data);
                }
            }
        } catch (Exception e) {
        	errormsg = e.getMessage();
        } finally {
        }
    	if(mListener != null)
    		mListener.onEndResponse(mEntity);
        if(!TextUtils.isEmpty(errormsg)) {
        	mEntity.setMessage(errormsg);
        	mHandler.sendEmptyMessage(Entity.ERROR);
        	LogcatUtils.e(TAG, errormsg);
        } else {
        	mHandler.sendEmptyMessage(Entity.FINISHED);
        }
    }
	
	protected boolean getResponseIsStream() {
		return mEntity.getGetType() == GetType.BYTES;
	}
	
	protected byte[] getGZipData(HttpURLConnection connection) throws Exception {
        InputStream stream = connection.getInputStream();
        CheckedInputStream csumi = new CheckedInputStream(stream, new CRC32());
        GZIPInputStream zipSteam = new GZIPInputStream(csumi);
        BufferedInputStream bi = new BufferedInputStream(zipSteam);
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[Task.BUFFER_SIZE];
        int readLen = -1;
        readLen = bi.read(buffer);
        while (readLen != -1) {
            if (isCanceled) {
                connection.disconnect();
                byteStream = null;
                break;
            } else {
                byteStream.write(buffer, 0, readLen);
            }
            readLen = bi.read(buffer);
        }
        try {
            if (byteStream != null) {
                byteStream.flush();
            }
            buffer = null;
            if (bi != null) {
                bi.close();
            }
            if (zipSteam != null) {
                zipSteam.close();
            }
            if (csumi != null) {
                csumi.close();
            }
            if (stream != null) {
                stream.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        byte[] bytes = (byteStream == null) ? null : byteStream.toByteArray();
        if (byteStream != null) {
            byteStream.close();
        }
        return bytes;
    }
	
	protected byte[] getStreamData(HttpURLConnection connection) throws Exception {
		ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        InputStream stream = connection.getInputStream();
        byte[] buffer = new byte[BUFFER_SIZE];
        int readLen = -1;
        readLen = stream.read(buffer);
        int blockNum = 0;
        while (readLen != -1) {
            if (isCanceled) {
                connection.disconnect();
                byteStream = null;
                break;
            } else {
                byteStream.write(buffer, 0, readLen);
            }
            if ((blockNum + 1) % BUFFER_COUNT == 0) {
                Message msg = Message.obtain();
                msg.what = Entity.DOWNLOADPROGRESS;
                msg.arg1 = blockNum * BUFFER_SIZE + readLen;
                msg.arg2 = connection.getContentLength();
                mHandler.sendMessage(msg);
            }
            blockNum++;
            readLen = stream.read(buffer);
        }
        try {
            if (byteStream != null) {
                byteStream.flush();
            }
            buffer = null;
            if (stream != null) {
                stream.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return (byteStream == null) ? null : byteStream.toByteArray();
	}
	
	@SuppressWarnings("deprecation")
	protected void parseResponseStream(byte[] data) {
		try {
	        if(mListener != null) {
	        	String result = new String(data, HTTP.UTF_8);
	        	LogcatUtils.i(TAG, "response result = " + result);
	        	TPARSED parsedata = mListener.onProcessData(mEntity, result);
	        	mEntity.setParsedData(parsedata);
	        }
		} catch (Exception e) {
			LogcatUtils.e(TAG, e.getMessage());
		}
	}
	
	protected void handleDownloadProgress(int current, int total) {
		mEntity.setDownloadProgress(current, total);
		if (mListener != null)
			mListener.onDownloadProgress(mEntity);
	}
	
	protected void handleUploadProgress(int current, int total) {
		mEntity.setUploadProgress(current, total);
		if (mListener != null)
			mListener.onUploadProgress(mEntity);
	}

	protected void setDownloadProgress(int current, int total) {
		Message msg = Message.obtain();
        msg.what = Entity.DOWNLOADPROGRESS;
        msg.arg1 = current;
        msg.arg2 = total;
        mHandler.sendMessage(msg);
	}

	protected void setUploadProgress(int current, int total) {
		Message msg = Message.obtain();
        msg.what = Entity.UPLOADPROGRESS;
        msg.arg1 = current;
        msg.arg2 = total;
        mHandler.sendMessage(msg);
	}
	
	protected void destroyHttpConnection(HttpURLConnection conn) {
        if (conn != null) {
            try {
                conn.disconnect();
            } catch (Exception e) {
            	LogcatUtils.e(TAG, e);
            }
            conn = null;
        }
    }
}
